fix(hooks): repair a stale SessionEnd hook instead of only installing once - #7
Conversation
… once The hook command embeds an absolute path to the binary, so renaming the project (claude-recall -> code-recall) left existing installs pointing at a command that no longer exists. The migration for exactly this case was already here — LEGACY_COMMAND_NAMES and _hook_mentions_app — but it sat behind `is_first_run`, which is false for anyone who has ever built an index. The rewrite branch was unreachable, so a broken hook stayed broken and failed silently on every session end. Split the two concerns: refresh an existing hook whenever it differs from the desired config, and only append a new one on first run, so a hook the user deleted is not resurrected. Writes happen only on an actual change, so ordinary runs still leave settings.json alone. Also stop destroying settings.json when it cannot be parsed. The read error was swallowed with `pass` and the resulting empty dict written straight back, replacing every unrelated key with just `hooks`. Bail out instead, and write through a temp file and atomic rename so an interrupted write cannot truncate the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fd9b75d32
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Auto-install hooks on first run | ||
| if is_first_run and not HOOKS_MARKER.exists(): | ||
| _auto_install_hooks() | ||
| # Repair a stale hook on every run, but only add a missing one on first run. |
There was a problem hiding this comment.
Bump the package version for this hook repair
This changes installed CLI behavior by repairing hooks during ordinary invocations, but pyproject.toml, src/code_recall/__init__.py, and uv.lock remain at version 0.2.5. Bump all three version records so installed copies and update checks can distinguish this bug fix.
AGENTS.md reference: AGENTS.md:L3-L11
Useful? React with 👍 / 👎.
| with open(tmp_path, "w") as f: | ||
| json.dump(settings, f, indent=2) | ||
| os.replace(tmp_path, settings_path) |
There was a problem hiding this comment.
Preserve settings.json permissions during atomic replacement
When an existing settings.json has restrictive permissions, writing a newly created .tmp file and replacing the original discards its mode; under a common 022 umask, a 0600 settings file becomes 0644. Because Claude settings can contain private configuration, preserve the original mode on the temporary file before os.replace.
Useful? React with 👍 / 👎.
Problem
The SessionEnd hook command embeds an absolute path to the binary, so renaming the project (
claude-recall→code-recall) left existing installs pointing at a command that no longer exists. The hook then failed silently on every session end:The migration for exactly this case was already present —
LEGACY_COMMAND_NAMESand_hook_mentions_app— but it was unreachable:Anyone past their first run could never reach the rewrite branch, so a broken hook stayed broken.
Changes
settings.jsonuntouched.settings.jsonwhen it cannot be parsed. The read error was swallowed withpassand the resulting empty dict written straight back, replacing every unrelated key with justhooks. Verified against the pre-fix code: a truncated settings file was left with only['hooks'], losingmodelandpermissions.Notes
_cmd_indexdoes not call_first_run_setup, so the hook's ownindex --quietnever rewrites settings — repair rides on interactive commands only. It has to: a hook pointing at a missing binary never executes and so cannot repair itself.Testing
11 new tests (339 total, all passing), plus
compileallandcheck_version.py. Confirmed as genuine regression tests by running the same scenario againstmain: old code leaves the stale command in place, new code repairs it while preserving unrelated keys.🤖 Generated with Claude Code